前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# 上传文件简单实现

30 秒速记

  • 链路:表单 enctype="multipart/form-data" 提交 → Koa 路由 → busboy 拆流 → fs.createWriteStream 落盘 → 返回 JSON
  • 前端表单少了 enctype,服务端只能收到文件名,收不到文件内容
  • 存储名服务端生成,客户端文件名只用来展示,防路径穿越和重名覆盖
  • 写流 finish 才算落盘成功;任何 error 都要删掉半截文件
  • 扩展名能伪造,类型校验看文件头字节(magic bytes),比如用 file-type 库

最简单的上传就是表单提交 multipart 请求,服务端用 busboy 把文件流 pipe 到磁盘,再返回结果。 这条链路十几行就能跑通,但离能上线还差几步:大小和数量限制要配,不然一个请求就能把磁盘写满;文件名要服务端生成,客户端传个 ../../app.js 进来就麻烦了;成功要以写流 finish 为准,不能看读流 end;类型不能只信后缀,要读文件头几个字节判断。我一般会先写到临时目录,校验通过再 rename 到正式目录。

理解「上传文件简单实现」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 filesystem 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

下面是一个可以直接替换的 uploadFile,基于 busboy 1.x,把限制、命名、成功判定和清理都补齐了。

const fs = require('fs')
const path = require('path')
const { randomUUID } = require('crypto')
const busboy = require('busboy')

const ALLOWED = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' }

function uploadFile(ctx, { dir }) {
  fs.mkdirSync(dir, { recursive: true }) // Node 10.12+ 自带递归创建

  return new Promise((resolve, reject) => {
    let bb
    try {
      bb = busboy({
        headers: ctx.req.headers,
        defParamCharset: 'utf8',
        limits: { fileSize: 5 * 1024 * 1024, files: 3, fields: 10 },
      })
    } catch (err) {
      return reject(Object.assign(err, { status: 415 }))
    }

    const fields = {}
    const tasks = []      // 每个文件一个 Promise,全部 finish 才算成功
    const written = []    // 出错时要删的文件

    bb.on('field', (name, val) => { fields[name] = val })

    bb.on('file', (name, file, { filename, mimeType }) => {
      const ext = ALLOWED[mimeType]
      if (!ext) return file.resume()           // 不要的也必须读掉
      const saveTo = path.join(dir, `${randomUUID()}.${ext}`)
      written.push(saveTo)
      const out = fs.createWriteStream(saveTo)

      tasks.push(new Promise((ok, fail) => {
        file.on('limit', () => fail(Object.assign(new Error('文件过大'), { status: 413 })))
        out.on('finish', () => ok({ field: name, originalName: filename, path: saveTo }))
        out.on('error', fail)
        file.on('error', fail)
      }))
      file.pipe(out)
    })

    bb.on('close', async () => {
      try {
        const files = await Promise.all(tasks)
        resolve({ success: true, fields, files })
      } catch (err) {
        written.forEach((p) => fs.rm(p, { force: true }, () => {}))
        reject(err)
      }
    })
    bb.on('error', reject)

    ctx.req.pipe(bb)
  })
}

module.exports = { uploadFile }
@前端进阶之旅: 代码已经复制到剪贴板

调用方在路由里 try { ctx.body = await uploadFile(ctx, { dir }) } catch (e) { ctx.status = e.status || 500 }。这里的 mimeType 来自客户端声明,只用来做第一道筛选;真正对外展示前,还要用 file-type 读文件头再确认一次。

面试官追问

追问 1前端表单忘了写 enctype,提交后后端会收到什么?
参考回答

默认是 application/x-www-form-urlencoded,文件 input 只会提交文件名字符串,内容根本没上传。busboy 解析出来只有一个普通字段,file 事件不会触发。

追问 2用户把文件名改成 ../../index.js 上传,会有什么风险?
参考回答

如果直接 path.join(uploadDir, filename),结果会跳出上传目录,可能覆盖服务代码。所以存储名一律用 crypto.randomUUID() 加白名单后缀生成,原始文件名只存数据库。

追问 3生产环境上传 5MB 的图片直接返回 413,Node 代码里根本没限制,怎么回事?
参考回答

多半是前面的 Nginx 拦了,client_max_body_size 默认是 1m。在对应 location 里调大,并且和 busboy 的 limits.fileSize 保持一致,别一层放一层收。

追问 4只允许上传图片,检查后缀是 .png 够吗?
参考回答

不够,后缀是用户随便改的。要读文件开头的字节,PNG 是 89 50 4E 47,JPEG 是 FF D8 FF,可以用 file-type 库判断;要显示给别人看的图片,最好再用 sharp 重新编码一遍,顺带把夹带的内容洗掉。

追问 5为什么建议先写临时目录,再移到正式目录?
参考回答

上传中途断开、超限、校验不过都会留下半截或非法文件,在临时目录里删就行,不会被静态服务提前对外暴露。校验通过后同一个文件系统内 fs.rename 是原子的,正式目录里永远只有完整文件。

# 上传文件简单实现

# 依赖模块

30 秒速记

  • 第三方只有一个 busboy,负责拆 multipart 流
  • 其余都是 Node 内置:fs 写文件、path 拼路径、util.inspect 打日志、os 可以拿 os.tmpdir() 放临时文件
  • util.inspect(val) 只适合打日志,拿它存字段值会多出一对引号
  • 手写递归建目录可以换成 fs.mkdirSync(dir, { recursive: true })

这个上传 demo 的依赖很轻:第三方只有 busboy,其他全是 Node 内置模块。 busboy 拆请求流,fs 负责创建目录和写文件,path 负责拼出跨平台的路径,util.inspect 用来把字段值打印得好看一点。有两个地方可以顺手改:inspect 会把字符串包上引号,'tom' 存进结果里前端拿到的就是带引号的值,存字段要直接用 val;手写的 mkdirsSync 递归函数在 Node 10.12 之后可以用 fs.mkdirSync(dir, { recursive: true }) 一行替代。

理解「依赖模块」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

面试官追问

追问 1返回的 formData.picName 是 "'cat'",多了一对单引号,哪来的?
参考回答

result.formData[fieldname] = inspect(val) 这句。util.inspect 是给日志用的,会把字符串格式化成带引号的形式。存值直接写 val,打日志才用 inspect。

追问 2代码里 require('os') 了但没用上,它本来能干什么?
参考回答

最常见的用法是 os.tmpdir()

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

上传文件简单实现|Nodejs专题系列

# 上传文件简单实现

30 秒速记

  • 链路:表单 enctype="multipart/form-data" 提交 → Koa 路由 → busboy 拆流 → fs.createWriteStream 落盘 → 返回 JSON
  • 前端表单少了 enctype,服务端只能收到文件名,收不到文件内容
  • 存储名服务端生成,客户端文件名只用来展示,防路径穿越和重名覆盖
  • 写流 finish 才算落盘成功;任何 error 都要删掉半截文件
  • 扩展名能伪造,类型校验看文件头字节(magic bytes),比如用 file-type 库

最简单的上传就是表单提交 multipart 请求,服务端用 busboy 把文件流 pipe 到磁盘,再返回结果。 这条链路十几行就能跑通,但离能上线还差几步:大小和数量限制要配,不然一个请求就能把磁盘写满;文件名要服务端生成,客户端传个 ../../app.js 进来就麻烦了;成功要以写流 finish 为准,不能看读流 end;类型不能只信后缀,要读文件头几个字节判断。我一般会先写到临时目录,校验通过再 rename 到正式目录。

理解「上传文件简单实现」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 filesystem 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

下面是一个可以直接替换的 uploadFile,基于 busboy 1.x,把限制、命名、成功判定和清理都补齐了。

const fs = require('fs')
const path = require('path')
const { randomUUID } = require('crypto')
const busboy = require('busboy')

const ALLOWED = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' }

function uploadFile(ctx, { dir }) {
  fs.mkdirSync(dir, { recursive: true }) // Node 10.12+ 自带递归创建

  return new Promise((resolve, reject) => {
    let bb
    try {
      bb = busboy({
        headers: ctx.req.headers,
        defParamCharset: 'utf8',
        limits: { fileSize: 5 * 1024 * 1024, files: 3, fields: 10 },
      })
    } catch (err) {
      return reject(Object.assign(err, { status: 415 }))
    }

    const fields = {}
    const tasks = []      // 每个文件一个 Promise,全部 finish 才算成功
    const written = []    // 出错时要删的文件

    bb.on('field', (name, val) => { fields[name] = val })

    bb.on('file', (name, file, { filename, mimeType }) => {
      const ext = ALLOWED[mimeType]
      if (!ext) return file.resume()           // 不要的也必须读掉
      const saveTo = path.join(dir, `${randomUUID()}.${ext}`)
      written.push(saveTo)
      const out = fs.createWriteStream(saveTo)

      tasks.push(new Promise((ok, fail) => {
        file.on('limit', () => fail(Object.assign(new Error('文件过大'), { status: 413 })))
        out.on('finish', () => ok({ field: name, originalName: filename, path: saveTo }))
        out.on('error', fail)
        file.on('error', fail)
      }))
      file.pipe(out)
    })

    bb.on('close', async () => {
      try {
        const files = await Promise.all(tasks)
        resolve({ success: true, fields, files })
      } catch (err) {
        written.forEach((p) => fs.rm(p, { force: true }, () => {}))
        reject(err)
      }
    })
    bb.on('error', reject)

    ctx.req.pipe(bb)
  })
}

module.exports = { uploadFile }
@前端进阶之旅: 代码已经复制到剪贴板

调用方在路由里 try { ctx.body = await uploadFile(ctx, { dir }) } catch (e) { ctx.status = e.status || 500 }。这里的 mimeType 来自客户端声明,只用来做第一道筛选;真正对外展示前,还要用 file-type 读文件头再确认一次。

面试官追问

追问 1前端表单忘了写 enctype,提交后后端会收到什么?
参考回答

默认是 application/x-www-form-urlencoded,文件 input 只会提交文件名字符串,内容根本没上传。busboy 解析出来只有一个普通字段,file 事件不会触发。

追问 2用户把文件名改成 ../../index.js 上传,会有什么风险?
参考回答

如果直接 path.join(uploadDir, filename),结果会跳出上传目录,可能覆盖服务代码。所以存储名一律用 crypto.randomUUID() 加白名单后缀生成,原始文件名只存数据库。

追问 3生产环境上传 5MB 的图片直接返回 413,Node 代码里根本没限制,怎么回事?
参考回答

多半是前面的 Nginx 拦了,client_max_body_size 默认是 1m。在对应 location 里调大,并且和 busboy 的 limits.fileSize 保持一致,别一层放一层收。

追问 4只允许上传图片,检查后缀是 .png 够吗?
参考回答

不够,后缀是用户随便改的。要读文件开头的字节,PNG 是 89 50 4E 47,JPEG 是 FF D8 FF,可以用 file-type 库判断;要显示给别人看的图片,最好再用 sharp 重新编码一遍,顺带把夹带的内容洗掉。

追问 5为什么建议先写临时目录,再移到正式目录?
参考回答

上传中途断开、超限、校验不过都会留下半截或非法文件,在临时目录里删就行,不会被静态服务提前对外暴露。校验通过后同一个文件系统内 fs.rename 是原子的,正式目录里永远只有完整文件。

# 上传文件简单实现

# 依赖模块

30 秒速记

  • 第三方只有一个 busboy,负责拆 multipart 流
  • 其余都是 Node 内置:fs 写文件、path 拼路径、util.inspect 打日志、os 可以拿 os.tmpdir() 放临时文件
  • util.inspect(val) 只适合打日志,拿它存字段值会多出一对引号
  • 手写递归建目录可以换成 fs.mkdirSync(dir, { recursive: true })

这个上传 demo 的依赖很轻:第三方只有 busboy,其他全是 Node 内置模块。 busboy 拆请求流,fs 负责创建目录和写文件,path 负责拼出跨平台的路径,util.inspect 用来把字段值打印得好看一点。有两个地方可以顺手改:inspect 会把字符串包上引号,'tom' 存进结果里前端拿到的就是带引号的值,存字段要直接用 val;手写的 mkdirsSync 递归函数在 Node 10.12 之后可以用 fs.mkdirSync(dir, { recursive: true }) 一行替代。

理解「依赖模块」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

面试官追问

追问 1返回的 formData.picName 是 "'cat'",多了一对单引号,哪来的?
参考回答

result.formData[fieldname] = inspect(val) 这句。util.inspect 是给日志用的,会把字符串格式化成带引号的形式。存值直接写 val,打日志才用 inspect。

追问 2代码里 require('os') 了但没用上,它本来能干什么?
参考回答

最常见的用法是 os.tmpdir(),拿到系统临时目录,先把上传文件写到这里,校验通过再移动到正式目录。不打算用就删掉,别留着让人猜。

追问 3拼路径为什么用 path.join 而不是字符串加 /?
参考回答

path.join 会处理多余的分隔符和 ..,在 Windows 上也会用正确的分隔符。但它不防路径穿越,path.join(dir, '../../x') 照样跳出去,所以文件名本身必须由服务端生成。

追问 4fs.existsSync 加 fs.mkdirSync 的递归写法,有什么问题?
参考回答

能用,但啰嗦,而且并发请求时两个请求同时判断不存在、同时创建,后一个会抛 EEXIST。fs.mkdirSync(dir, { recursive: true }) 目录已存在也不报错,一行解决,启动时建一次就够,不用每个请求都建。

# 安装依赖

30 秒速记

  • npm install busboy,它是运行时依赖,进 dependencies
  • 跟着本文的 new Busboy() 写法要装 busboy@0.3.1;装最新版就得按 1.x 的 API 写
  • koa-bodyparser 不处理 multipart,替代不了 busboy
  • 锁文件和包管理器保持一致,yarn.lock 和 package-lock.json 别混着提交

这一步就是 npm install busboy,它负责从请求里把文件流解析出来。 要提醒的是版本:本文代码是 0.x 的写法,new Busboy({ headers })、file 回调五个参数、finish 事件;直接 npm install busboy 装到的是 1.x,代码要跟着改,或者显式装 busboy@0.3.1。我自己的建议是新项目直接用 1.x,0.x 已经不维护了。另外 koa-bodyparser 和它不是一回事,前者只管 JSON 和普通表单。

← 7.1 busboy模块7.3 异步上传图片实现 →

fe